package com.gnerv.sylvanas.module.mybatis.extend;

import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Gnerv LiGen
 */
public class MybatisResourceHandler {

    private static final String TAG_NAME = "resultMap";
    private static final String EXTEND_NAME = "extend";
    private static final String EXTEND_ID_SUFFIX = "Extend";

    private final Map<String, List<DtoExtendResult>> dtoExtendResultBeans;
    private final Resource[] resources;

    public MybatisResourceHandler(List<DtoExtendResult> dtoExtendResultBeans, Resource[] resources) {
        this.dtoExtendResultBeans = this.dtoExtendResultBeans(dtoExtendResultBeans);
        this.resources = resources;
    }

    public Resource[] build() {
        Resource[] rs = new Resource[this.resources.length];
        for (int i = 0; i < resources.length; i++) {
            Resource resource = handle(resources[i]);
            rs[i] = resource;
        }
        return rs;
    }

    public Resource handle(Resource resource) {
        return updateResource(resource);
    }

    private Resource updateResource(Resource resource) {
        Document document = convertToDom(resource);
        NodeList resultMap = document.getElementsByTagName(TAG_NAME);
        for (int i = 0; i < resultMap.getLength(); i++) {
            Element node = (Element) resultMap.item(i);
            String type = node.getAttribute("type");
            if (dtoExtendResultBeans.containsKey(type)) {
                Class<?> clazz = null;
                try {
                    clazz = ClassUtils.forName(type, ClassUtils.getDefaultClassLoader());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
                List<Element> dtoExtendResultMapElement = createDtoExtendResultMapElement(document, type);
                boolean assignableFrom = Map.class.isAssignableFrom(clazz);
                if (assignableFrom) {
                    // 基于 BaseDTO 继承 HashMap 扩展
                    for (Element element : dtoExtendResultMapElement) {
                        node.appendChild(element);
                    }
                } else {
                    // 基于 Map 类型的 extend 属性 扩展
                    Element dtoExtendResultMap = createDtoExtendResultMap(document, type);
                    for (Element element : dtoExtendResultMapElement) {
                        dtoExtendResultMap.appendChild(element);
                    }
                    Element extendResult = createExtendResult(document, dtoExtendResultMap.getAttribute("id"));
                    node.appendChild(extendResult);

                    NodeList mapper = document.getElementsByTagName("mapper");
                    Element mapperNode = (Element) mapper.item(0);
                    mapperNode.appendChild(dtoExtendResultMap);
                }
                resource = toResource(document);
            }
        }
        return resource;
    }

    private Resource toResource(Document document) {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = null;
        try {
            transformer = transformerFactory.newTransformer();
        } catch (TransformerConfigurationException e) {
            throw new RuntimeException(e);
        }
        // 解决中文问题
        transformer.setOutputProperty("encoding", "UTF-8");

        transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
                document.getDoctype().getSystemId());
        transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
                document.getDoctype().getPublicId());
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            transformer.transform(new DOMSource(document), new StreamResult(byteArrayOutputStream));
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
        InputStream sbs = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        return new InputStreamResource(sbs);
    }

    private Element createDtoExtendResultMap(Document document, String type) {
        String id = type.substring(type.lastIndexOf(".") + 1) + EXTEND_ID_SUFFIX;
        Element dtoExtendResultMap = document.createElement(TAG_NAME);
        dtoExtendResultMap.setAttribute("id", id);
        dtoExtendResultMap.setAttribute("type", "java.util.HashMap");
        return dtoExtendResultMap;
    }

    private List<Element> createDtoExtendResultMapElement(Document document, String type) {
        List<DtoExtendResult> dtoExtendResults = dtoExtendResultBeans.get(type);
        List<Element> elementList = new ArrayList<>();
        for (DtoExtendResult dtoExtendResult : dtoExtendResults) {
            String methodFullName = dtoExtendResult.getMethod();
            String className = methodFullName.substring(0, methodFullName.lastIndexOf("."));
            String methodName = methodFullName.substring(methodFullName.lastIndexOf(".") + 1);
            Class<?> clazz = null;
            try {
                clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            Method method = null;
            try {
                method = clazz.getMethod(methodName);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
            Class<?> returnType = method.getReturnType();
            boolean asList = Collection.class.isAssignableFrom(returnType);
            if (asList) {
                /**
                 *         <collection property="dictItemList" column="{dictId=uk_id,name=name}"
                 *                     ofType="com.embrace.boot.base.dto.DictItemDTO"
                 *                     select="com.embrace.boot.base.mapper.DictItemMapper.listDictItemByDictId">
                 *         </collection>
                 */
                Element collection = document.createElement("collection");
                collection.setAttribute("property", dtoExtendResult.getKey());
                collection.setAttribute("column", dtoExtendResult.getColumn());
                collection.setAttribute("ofType", dtoExtendResult.resultType());
                collection.setAttribute("select", dtoExtendResult.getMethod());
                elementList.add(collection);
            } else {
                /**
                 *         <association property="userInfo" column="uk_id" javaType="com.gnerv.pabu.basic.core.dto.UserInfoDTO"
                 *                      select="com.gnerv.pabu.basic.core.mapper.UserInfoMapper.selectUserInfo">
                 *         </association>
                 */
                Element collection = document.createElement("association");
                collection.setAttribute("property", dtoExtendResult.getKey());
                collection.setAttribute("column", dtoExtendResult.getColumn());
                collection.setAttribute("javaType", dtoExtendResult.resultType());
                collection.setAttribute("select", dtoExtendResult.getMethod());
                elementList.add(collection);
            }
        }
        return elementList;
    }


    private Element createExtendResult(Document document, String resultMap) {
        Element extendResult = document.createElement("collection");
        extendResult.setAttribute("property", EXTEND_NAME);
        extendResult.setAttribute("resultMap", resultMap);
        return extendResult;
    }

    private Document convertToDom(Resource resource) {
        try {
            InputSource inputSource = new InputSource(resource.getInputStream());
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setValidating(true);
            factory.setNamespaceAware(false);
            factory.setIgnoringComments(true);
            factory.setIgnoringElementContentWhitespace(false);
            factory.setCoalescing(false);
            factory.setExpandEntityReferences(true);

            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(new XMLMapperEntityResolver());
            builder.setErrorHandler(new ErrorHandler() {
                @Override
                public void error(SAXParseException exception) throws SAXException {
                    throw exception;
                }

                @Override
                public void fatalError(SAXParseException exception) throws SAXException {
                    throw exception;
                }

                @Override
                public void warning(SAXParseException exception) throws SAXException {
                    // NOP
                }
            });
            return builder.parse(inputSource);
        } catch (Exception e) {
            throw new BuilderException("Error creating document instance.  Cause: " + e, e);
        }
    }

    private Map<String, List<DtoExtendResult>> dtoExtendResultBeans(List<DtoExtendResult> dtoExtendResultList) {
        Map<String, List<DtoExtendResult>> dtoExtendResultBeans = new HashMap<>();
        for (DtoExtendResult dtoExtendResult : dtoExtendResultList) {
            String name = dtoExtendResult.getMain().getName();
            List<DtoExtendResult> dtoExtendResults = dtoExtendResultBeans.get(name);
            if (ObjectUtils.isEmpty(dtoExtendResults)) {
                dtoExtendResults = new ArrayList<>();
            }
            dtoExtendResults.add(dtoExtendResult);
            dtoExtendResultBeans.put(name, dtoExtendResults);
        }
        return dtoExtendResultBeans;
    }


}
